09-技术难点与避坑指南
# 第9章 技术难点与避坑指南
## 9.1 编译缓存相关
### 坑1: 修改了hook文件但页面没有变化
**原因**: `DEBUG`模式不是2时,`tmp/`缓存不会自动更新。
**解决方案**:
- 开发阶段确保`DEBUG=2`
- 手动删除`tmp/`目录下所有文件
- 在后台禁用再启用插件(会触发`plugin_clear_tmp_dir()`)
### 坑2: conf.json修改后不生效
**原因**: `conf.json`在`plugin_init()`时一次性加载,运行期间不会重新读取。
**解决方案**: 修改`conf.json`后需要重启PHP进程或清空tmp缓存。
### 坑3: _include()编译失败导致白屏
**原因**: hook文件中有PHP语法错误,编译后的文件无法被include。
**解决方案**:
- 检查`tmp/`目录下编译后的文件,定位语法错误
- 确保hook PHP文件以``)
- 确保hook文件内容与目标hook点的上下文语法兼容
## 9.2 Hook机制相关
### 坑4: hook文件名与hook点不匹配
**原因**: hook文件名必须与源码中的hook标记完全一致(包括大小写和扩展名)。
**示例**:
```
源码标记: // hook model_inc_file.php
hook文件: plugin/my_plugin/hook/model_inc_file.php ✓
hook文件: plugin/my_plugin/hook/model_inc_file.PHP ✗ (大小写不匹配)
hook文件: plugin/my_plugin/hook/model_inc_file ✗ (缺少扩展名)
```
**解决方案**: 仔细核对hook文件名与源码标记,确保完全一致。
### 坑5: model_inc_file.php hook末尾缺少逗号
**原因**: 这个hook的内容被插入到`$include_model_files`数组中,缺少逗号会导致PHP语法错误。
```php
// ✗ 错误 - 缺少逗号
```
## 9.3 数据库相关
### 坑9: ALTER TABLE重复执行报错
**原因**: `install.php`在重新安装时会再次执行,如果字段已存在会报错。
**解决方案**:
```php
// 方法1: 使用IF NOT EXISTS(仅适用于CREATE TABLE)
$sql = "CREATE TABLE IF NOT EXISTS ...";
// 方法2: 先检查字段是否存在
$columns = db_find_index('thread');
$col_names = array_column($columns, 'Column_name');
if (!in_array('my_field', $col_names)) {
db_exec("ALTER TABLE {$tablepre}thread ADD COLUMN my_field int(11) DEFAULT '0'");
}
```
### 坑10: db_find多值条件使用OR而非IN
**原因**: XiunoPHP的SQL构建器将多值条件展开为OR,而非使用IN:
```php
db_find('my_table', array('uid'=>array(1, 2, 3)));
// 生成: WHERE uid=1 OR uid=2 OR uid=3
// 而非: WHERE uid IN (1,2,3)
```
**影响**: 当值很多时,SQL语句过长,性能下降。
**解决方案**: 手动编写SQL:
```php
$uids = implode(',', array_map('intval', $uid_array));
$sql = "SELECT * FROM {$tablepre}my_table WHERE uid IN ($uids)";
$datalist = db_sql_find($sql);
```
### 坑11: db_update增量更新的键名格式
**原因**: 增量更新的键名以`+`/`-`结尾:
```php
db_update('user', array('uid'=>$uid), array('credits+'=>10));
// 生成: UPDATE bbs_user SET credits=credits+10 WHERE uid=123
```
**注意**: 键名中的`+`/`-`是XiunoPHP的特殊语法,不是PHP标准语法。
### 坑12: 无外键约束导致的数据不一致
**原因**: Xiuno BBS不使用外键约束,删除数据时需手动清理关联记录。
**解决方案**: 在`model_xxx_delete_start.php`或`model_xxx_delete_end.php` hook中清理:
```php
// hook/model_thread_delete_end.php
db_delete('my_table', array('tid'=>$tid));
```
### 坑13: 密码安全 - MD5+Salt已被淘汰
**原因**: Xiuno BBS使用MD5+Salt加密密码,已被证明不安全。
**影响**: 插件中涉及密码操作时,不要依赖核心的密码机制。如果需要独立认证,使用`password_hash()`/`password_verify()`。
## 9.4 安全相关
### 坑14: XSS防护不完整
**原因**: `param()`默认使用`htmlspecialchars`防XSS,但某些场景不够:
- 富文本内容需要更严格的过滤
- JavaScript中直接输出PHP变量需要额外编码
**解决方案**:
```php
// PHP输出到JS时使用json_encode
// 富文本过滤
$safe_html = xn_html_safe($html_content);
```
### 坑15: SQL注入风险
**原因**: XiunoPHP使用`addslashes()`防注入(非预处理),GBK编码下可能被绕过。
**解决方案**:
- 始终使用`param()`获取用户输入
- 使用`db_create/db_update/db_delete`等封装函数
- 手动SQL时使用`addslashes()`或参数化查询
- 确保数据库使用UTF-8编码
### 坑16: CSRF防护
**原因**: Xiuno BBS使用`form_hash`进行CSRF防护。
**解决方案**: 表单中必须包含:
```html
```
AJAX请求中也需要携带:
```javascript
$.xpost(url, {form_hash: xn.form_hash, ...}, callback);
```
### 坑17: 文件上传安全
**原因**: 直接使用`$_FILES`时缺少安全检查。
**解决方案**:
- 使用Xiuno的附件上传接口`attach-create`
- 检查文件类型白名单
- 使用`image_safe_name()`生成安全文件名
- 使用`image_set_dir()`创建分级目录
## 9.5 兼容性相关
### 坑18: 主题覆盖插件UI
**原因**: 主题的overwrite可以覆盖其他插件的hook文件和模板,导致插件功能异常。
**解决方案**:
- 插件UI尽量使用独立的hook点,避免与主题冲突
- 在主题文档中声明插件兼容性
- 使用`hooks_rank`控制注入顺序
### 坑19: 子目录部署路径问题
**原因**: Xiuno BBS支持子目录部署,硬编码路径会导致资源加载失败。
**解决方案**:
- 静态资源使用`./`相对路径
- URL使用`url()`函数生成
- 资源路径使用`$conf['view_url']`
### 坑20: PHP版本兼容性
**原因**: 不同PHP版本的函数和行为有差异。
**注意事项**:
- PHP 7.2+: 支持`object`类型提示
- PHP 8.0+: `str_contains()`/`str_starts_with()`等新函数
- PHP 8.1+: `mysqli`默认错误模式改变
- PHP 8.2+: 动态属性弃用
**解决方案**: 插件应声明最低PHP版本要求,使用兼容性写法。
## 9.6 性能相关
### 坑21: 循环内查询数据库 (N+1问题)
**原因**: 在循环中调用`user_read()`等函数,每次都查询数据库。
**解决方案**:
```php
// ✗ 错误 - N+1查询
foreach ($threadlist as $thread) {
$user = user_read($thread['uid']); // 每次查数据库
}
// ✓ 正确 - 批量查询
$uids = arrlist_values($threadlist, 'uid');
$userlist = user_find_by_uids(implode(',', $uids));
foreach ($threadlist as &$thread) {
$thread['user'] = $userlist[$thread['uid']];
}
```
### 坑22: 缓存未及时清理
**原因**: 更新数据后忘记清理缓存,导致显示旧数据。
**解决方案**:
```php
function my_table_update($id, $arr) {
$r = my_table__update($id, $arr);
cache_delete('my_table_' . $id); // 清理单条缓存
cache_delete('my_table_list'); // 清理列表缓存
return $r;
}
```
### 坑23: 大表ALTER TABLE锁表
**原因**: 给大表添加字段时,ALTER TABLE会锁表导致服务不可用。
**解决方案**:
- 使用`pt-online-schema-change`工具(Percona Toolkit)
- 在低峰期执行
- 考虑使用独立的关联表代替ALTER TABLE
## 9.7 调试技巧
### 技巧1: 查看编译后的文件
```php
// 编译后的文件在 tmp/ 目录下
// 如 route/index.php → tmp/route_index.php
// 如 view/htm/thread.htm → tmp/view_htm_thread.htm
```
直接查看编译后的文件,可以看到hook注入后的完整代码。
### 技巧2: 使用message()调试
```php
message(-1, '调试: uid=' . $uid . ', tid=' . $tid);
```
AJAX请求会返回JSON,普通请求会显示消息页面。
### 技巧3: 临时变量输出
```php
// 在hook文件中
var_dump($variable);
exit;
```
### 技巧4: 检查Hook是否被正确注入
查看`tmp/`目录下编译后的文件,搜索hook文件名确认是否被注入。
### 技巧5: 日志记录
```php
// 写入日志到 tmp/ 目录
file_put_contents_try(APP_PATH.'tmp/debug.log', date('H:i:s')." ".$message."\n", FILE_APPEND);
```